Harden report output against stored-XSS via markdown#195
Conversation
A security tool should not emit an injectable report. Findings from external engines can carry attacker-influenced text (a scanned file's `<script>` in a message); when a generated markdown report is rendered as HTML that becomes stored XSS. - appguardrail_core/reports.py: _sanitize_for_markdown() HTML-escapes the prose fields (message/remediation/verification) and neutralizes ``` code-fence breakout in snippets, applied at every report normalization site (covers all report types). Constrained identifiers (rule_id/category/context/severity), which also drive gate logic, are left untouched. - Tests: tests/test_report_sanitization.py (no raw <script>/<iframe>/<b> in any report type, entity escaping, fence-breakout neutralized, benign text unchanged / no over-escaping). - Verified: full suite 208 passed (benign existing messages unaffected). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EywwS2Du8pimW7xqRP3An3
9358b6b to
e993d83
Compare
OpenCode Review Overview
Pull request overviewOpenCode reviewed the current-head bounded evidence and found no blocking issues. FindingsNo blocking findings. SummaryApproval sufficiency: bounded evidence supplied affirmative approval evidence for changed files, coverage/docstring posture, risk surfaces, and current-head verification; approval is not based merely on the absence of known blockers.
Changed-File Evidence Mapflowchart LR
PR["PR changed files"] --> Evidence["OpenCode bounded evidence"]
Evidence --> S1["Changed file (2 files)"]
S1 --> I1["repository behavior"]
I1 --> R1["Review risk: Changed file (2 files)"]
R1 --> V1["required checks"]
Evidence --> S2["Test: test_report_sanitization.py"]
S2 --> I2["regression suite"]
I2 --> R2["Review risk: Test: test_report_sanitization.py"]
R2 --> V2["targeted test run"]
|
There was a problem hiding this comment.
Pull request overview
OpenCode reviewed the current-head bounded evidence and found no blocking issues.
Findings
No blocking findings.
Summary
Approval sufficiency: bounded evidence supplied affirmative approval evidence for changed files, coverage/docstring posture, risk surfaces, and current-head verification; approval is not based merely on the absence of known blockers.
Verification posture: CodeGraph evidence was initialized and bounded current-head evidence reviewed for changed-file evidence including CHANGELOG.md, appguardrail_core/reports.py, tests/test_report_sanitization.py.
Linter/static: workflow/static review evidence is bounded by the current-head GitHub Checks gate and changed-file evidence.
TDD/regression: coverage execution evidence and focused changed hunks were reviewed from bounded-review-evidence.md.
Coverage: coverage execution evidence reports supported repository test suites passed.
Docstring coverage: coverage execution evidence reports configured repository docstring gates passed or docstring coverage was advisory.
DAG: CodeGraph/source-backed behavior map connects CHANGELOG.md to the affected review, runtime, or workflow path and required checks.
PoC/execution: coverage-evidence job executed on the current head and reported PASS.
DDD/domain: workflow and repository-governance invariants were reviewed against changed files in bounded evidence.
CDD/context: CodeGraph evidence, changed-file history, and focused hunks were reviewed from bounded-review-evidence.md.
Similar issues: changed-file history evidence was reviewed for comparable local precedents.
Claim/concept check: bounded evidence, repository source, current-head workflow evidence, and, where numeric, scientific, statistical, or literature-backed claims are affected, original-paper/formula evidence and parameter-recovery expectations were used for claims.
Standards search: standards and external-source checks are delegated to configured OpenCode web_search/Context7/DeepWiki sources when applicable; no evidence-backed standards blocker is present in bounded evidence.
Compatibility/convention: changed workflow/script conventions, object naming, and reserved-word safety for schema/API/config/code surfaces were checked in bounded evidence.
Breaking-change/backcompat: deployment evidence and changed-file history were checked for backward-compatibility risk.
Performance: changed surfaces were checked for performance risk in bounded evidence.
Developer experience: changed automation, review, test, setup, and maintenance surfaces were checked for helpful or obstructive DX impact in bounded evidence.
User experience: connected user, operator, API, CLI, documentation, review-comment, status-check, rendering, and workflow-reader behavior was checked for contradictions against code, docs, and tests in bounded evidence.
Visual/DOM: Playwright visual, DOM locator, ARIA snapshot, console, and responsive evidence were checked when a web UI surface was present; for non-web surfaces, API/CLI/log/docs/workflow interaction evidence was reviewed instead.
Accessibility/i18n: accessibility, localization, and human-readable text surfaces were checked where UI, CLI, API message, docs, logs, or review text changed.
Supply-chain/license: dependency, package, model, container, and external-tool changes were checked in bounded evidence.
Packaging: package, build, test, lint, and security contracts were checked in bounded evidence.
Security/privacy: workflow-token, review-gate, and repository-automation security/privacy boundaries were checked in bounded evidence.
- Result: APPROVE
- Reason: Security hardening with comprehensive test coverage
- Head SHA:
3cf67b1c32688e4e2b2114c815d16dff76d2aba8 - Workflow run: 29085685320
- Workflow attempt: 1
Changed-File Evidence Map
flowchart LR
PR["PR changed files"] --> Evidence["OpenCode bounded evidence"]
Evidence --> S1["Changed file (2 files)"]
S1 --> I1["repository behavior"]
I1 --> R1["Review risk: Changed file (2 files)"]
R1 --> V1["required checks"]
Evidence --> S2["Test: test_report_sanitization.py"]
S2 --> I2["regression suite"]
I2 --> R2["Review risk: Test: test_report_sanitization.py"]
R2 --> V2["targeted test run"]
Summary
A security tool shouldn't emit an injectable artifact. Report renderers interpolate finding fields into markdown; findings from external engines (Trivy/Bandit/Semgrep) can carry attacker-influenced text — a scanned file's
<script>landing in amessage. When that markdown report is later rendered as HTML (GitHub, a viewer, a PDF pipeline), it's stored XSS._sanitize_for_markdown()returns a render-safe copy: prose fields (message/remediation/verification) HTML-escaped, andsnippetneutralized so it can't break out of its ``` code fence. Applied at every report normalization site, so all report types (buyer-diligence, founder-friendly, agency, fix-pack) are covered.rule_id/category/context/severity) — which also drive gate logic — are intentionally left untouched.Found by the repo's own
strixscan flagging unsanitized interpolation in report rendering — fixing it at the shared boundary rather than per call site.Test plan
pytest— 208 passed, incl.tests/test_report_sanitization.py(no raw<script>/<iframe>/<b>in any report type, entity escaping, fence-breakout neutralized, benign messages unchanged — no over-escaping)Follow-up: the
compliancereport (PR #192) will adopt_sanitize_for_markdownonce both land.🤖 Generated with Claude Code